第一次看到 TypeScript 的人,最先看到就是型別註記(以下會把程式碼有註記的部分用 ~
顯示出來)—— 英文稱為 Type Annotation —— 很重要,因為要查找任何跟型別系統有關的資源,這單字會不停地出現。
不過這些都還只是非常非常表面而已,型別系統(Type System)的用意就是要讓開發者能夠發現我們到底不小心在哪裡寫錯程式碼,除錯過程較為簡單外,也不需要很煩悶的不停執行 -> 看錯誤訊息 -> 翻程式碼 -> 好!找到了!改!-> 然後結果下次執行後還有下一段錯誤 -> 看錯誤訊息 -> 翻程式碼 -> WTx!原來是這裡出錯!再改!
讀者後來會發現本系列,講解語法的過程中很少會需要編譯,目的是要能夠讓 TypeScript 編譯器自動指出我們寫錯的地方
但是要能夠掌握這個系統,寫出穩紮穩打的程式碼,勢必要理解型別的推論(Inference)以及註記(Annotation)的原理以及應用時機:
Type Inference 與 Annotation 的 4W1H
What:推論與註記到底為何?
Where:到底什麼地方會用到推論與註記?
When:到底什麼時候會用到推論與註記?
Why:為何我們要認識推論與註記的機制?
How:如何善用推論與註記?
本日正文開始!
在回答型別推論與註記在 TypeScript 運作的機制之前,要先知道 TypeScript 內建定義了哪些基礎型別(Types)。
筆者大致上劃分了幾個類別,個人覺得倒是挺多種的(汗水看了不停直流):
number
, string
, boolean
, undefined
, null
, ES6 介紹的 symbol
與時常會在函式型別裡看到的 void
string
和 number
型別組合成的):
Array<T>
或T[]
),類別以及類別產出的物件(也就是 Class 以及藉由 Class new
出來的 Instance)(input) => (Ouput)
這種格式的型別,後面會再多做說明"Hello world" —— 若成為某變數的型別的話,它只能存剛好等於
"Hello world"` 字串值;但通常會看到的是 Object Literal Type,後面也會再多做說明any
、never
(TS 2.0釋出)以及最新的 unknown
型別(TS 3.0釋出),讀者可能覺得莫名其妙,不過這些型別的存在仍然有它的意義,而且很重要,陷阱總是出現在不理解這些特殊型別的特性
union
與 intersection
的型別組合,但是跟其他的型別的差別在於:這類型的型別都是由邏輯運算子組成,分別為 |
與 &
[2019.09.21 新增] 貼心小提示
想要跳到不同型別種類的推論與機制,這裡筆者整理出連結:
這邊給讀者們總覽 TypeScript 支援的型別,後續將會解釋以上型別的正確運用方式。
void
另外,筆者針對為何將
void
放到原始型別作一個補充:void
在筆者看來也可以放置在特殊型別的定義裡,但是由於它具有明確的意思 —— 代表某函數為不回傳值的狀態 —— 和undefined
有點類似,因為函數不回傳任何東西就跟回傳undefined
有點相近,不過在習慣上,我們都會以void
作為函數不回傳任何值的代表性特徵喔。(Day 04. 會再詳細說明)
“作者一直提醒一直講:型別推論與註記,雖然註記部分看起來有介紹到,但到現在還沒看到型別的推論(Inference)啊!”(拍桌)
這裡先跟大家說明,型別的推論到底什麼時候會用到呢?
其實呢,使用者也不需要主動去用,一直以來,TypeScript 就在為您關照、推論那你寫的變數的型別,自動幫您監測它們!聽起來很窩心吧~* ^_^ *~
所以型別推論是 TypeScript 的被動技!
然而,使用者也是會讓 TypeScript 感到沒輒的時候,就算是 TypeScript,它也沒辦法完全進到你的心幫你把功能都確認好,所以我們要培養好習慣,和 TS 成為好兄弟,而不是把 TS 當煙蒂來看待。(不要隨意使用 any
)
那這裡就有問題了,TS 到底是怎麼猜你寫的型別到底是什麼呢?後面就要進行程式碼的講解,不過我們先建好環境。
首先,上一篇已經創建好的 typescript-tutorial
資料夾內部,再開新的資料夾 —— 筆者取其名為 01-basic
。(打開編輯器結果如圖一所示)
圖一:新增 01-basic
資料夾在我們的 typescript-tutorial
資料夾裡
貼心小提示
不想再另開終端機的話,可能有些人早就知道要如何處置,不過這裡筆者還是提醒一下。
我們可以在 VSCode 上方有一個名為
Terminal
的選項開啟終端機。該終端機介面會在編輯器下方的面板裡連上。(如圖二)
圖二:點擊上方 Terminal 選項部分新增終端機的介面在我們的 IDE 裡
記得,我們必須進到 01-basic
這個資料夾裡面喔!不是在 typescript-tutorial
那一層!然後這是第二次提醒如何初始化 TypeScript 編譯器的設定檔,往後筆者會快速提及並且跳過建置環境的細節,除非我們要使用其它特定的模組或對 tsconfig
進行特殊設定。(結果如圖三)
// 沒有進到 01-basic 資料夾記得:
$ cd ./01-basic
// 初始化 TS 編譯器設定檔
$ tsc --init
圖三:下達基礎指令過後,蹦出的 tsconfig.json
檔案
[2019.09.15 新增] tsconfig.json 設定
這裡筆者必須緊急說明:若讀者試著筆者舉的程式碼範例的話,請讀者記得將裡面的
strictNullCheck
選項改成true
,這一點忘記在文章系列的一開頭提醒讀者,實在是很抱歉!/* tsconfig.json */ { "compilerOptions": { /* ... */ "strictNullChecks": true, /* ... */ } }
因此請讀者注意,目前學習的 TypeScript 型別系統版本多了一個
strictNullCheck
的編譯器屬性設定!至於為何會造成如此狀況,那是因為筆者在專案上習慣將某些 TypeScript 編譯器設定啟動!至於strictNullCheck
到底為何,將會在型別系統講述告一段落後,開始講述 TypeScript 的編譯器設定檔喔![2019.09.18 新增] 程式碼範例
如果想要看到本系列文裡面舉的程式碼範例可以參考 Maxwell-Alexius/Iron-Man-Competition 這個 GitHub Repo 喔~寫作過程當中會不斷更新的!
好的,一開始先建立好 index.ts
,再來開始寫程式碼吧!
首先要把基礎的東西講清楚,相信讀者也會認為這邊都很簡單(呼呼~等到介紹 interface
以及 class
才會很精彩),因此可以快速看過。
當然,我們可以看到下面這段程式碼編譯過後可以正常執行:
// index.ts
let myName = 'Maxwell';
let age = 20;
let hasPet = false;
let nothing = undefined;
let nothingLiterally = null;
不過這裡我們要注意一個重點 —— 筆者並沒有對這些變數的定義做型別的註記(Type Annotation),但是我們可以在 IDE 上開始呼叫我們的變數名稱,譬如圖四所示。
圖四:呼叫我們的 myName
變數
結果 TS 除了認得定義過後的變數外,也認得該變數型別是 string
(出現了 let myName: string
的說明),這個辨識推論型別的行為就是所謂的型別推論(Type Inference)。
讀者試試看
試試看其他的變數,比如
hasPet
這個變數,結果也會順順利利地推論出為boolean
的型別。不過到最後你會發現一些有趣的現象。
這時候我們就卡關了 —— 我們如果對 nothing
或 nothingLiterally
這兩個型別作確認的話,結果 TypeScript 會把它們推論成 any
型別(如圖五與圖六)。
圖五:nothing
被 TS 當成 any
圖六:nothingLiterally
也被 TS 當成 any
這種 null
跟 undefined
類型的東西英文又被稱為 Nullable Types,我們會在後續跟 TS 編譯器設定部分章節會再次提及,不過讀者先記好這種 Nullable Types 被認為是 any
型別的特性就 OK 了。
但是型別推論的真正用意是 —— 如果設法指派其他型別的值到被推論後的變數的話,TypeScript 會提醒你並顯示警告。(如圖七)
圖七:我們將字串丟進被 TS 認為是存 number
的變數裡
TS 直接對我們的行為提出質疑 —— 認為 age
這種東西應該要放數字,不是字串。
重點 1. 型別推論的目的 Purpose of Type Inference
讓 TypeScript 協助我們確保不會做錯事情 —— 也就是不小心把不同型別的東西丟到被推論過後的變數
讀者試試看
接下來,你可以試試看對其他變數做任何型別的存取動作,如果讀者細心一點,你也將會發現神奇的事情。
根據圖八的結果,會發現本來就被推論為基本的數字、字串或者布林代數型別的變數,在給予其他型別的值,不外乎都會被 TS 提出質疑(紅色的~~~~
所顯示部分)。
然而,Nullable Types 就是會跟著我們作對,我們對 Nullable Types(或者是被推論為 any
型別的變數)進行變數的指派動作都不會被 TS 視為警告。
圖八:針對不同的變數,隨便帶入不同型別的值進去所得出結果
更扯的是 —— 我們的 Nullable Types 或者被視為 any
型別的變數可以隨隨便便地使用。(如圖九)
圖九:any
型別愛怎麼變就怎麼變,TS 也拿它們沒輒
重點 2.
any
是造成型別混亂的根源Nullable Types 會被推論為
any
型別,而any
在 TypeScript 裡會無法監督變數狀態,造成程式碼的混亂。因此我們應當儘量避免這件事情的發生:變數的型別被視為
any
那讀者可能會問:『 我們有沒有情況會用到 any
呢? 』
極少數狀況會用到。
不過剛開始用 TypeScript,最好能夠避免就儘量避免,不然你沒有讓 TS 發揮真正的優勢的話,也根本就不需要 TS 幫你監督程式碼。(筆者表示:完全是開發者自律體現的哲學啊~)
其實還有另一種狀況會出現 any
,在英文裡稱這個現象為 Delayed Initialization(筆者想取一個很酷的中文名字,所以就稱它為遲滯性指派)。
這也挺常見,讀者當初學習原生 JS,定義變數時,可能早就碰過:你先定義變數後,不直接指派值,而是當程式碼執行到後面才開始指派。比如:
其中 TS 一開始就已經認定好 messageToSend
就是 any
型別。因此不管你後續代入什麼值進去,TS 都無所謂,完全放棄閒置狀態。(圖十跟圖十一)
圖十:一開始不跟 TS 講好是什麼東西的話,推論都會是 any
型別
圖十一:對於這種類型,TS 根本不想鳥你,實在是有夠狠
其實原理很簡單:你對剛定義出的任何變數沒有帶入值的話,就等同於帶入 undefined
這個值的概念,也就回到我們剛剛所講的 Nullable Type。因此我們可以得出第三個重點:
重點 3. 遲滯性指派 Delayed Initialization
每當我們對任何變數不立即指派值,該變數會無條件被視為
any
型別。
因此,為了避免有 any
型別的狀態發生,應當對這些被指派 Nullable Types 的變數或者不立即被指派值的變數做型別註記 —— 也正是我們剛開始提到的 Type Annotation。
如果將以上的程式碼寫入 TS 檔,你會發現 TS 會限制這兩個變數 —— 不被其他型別取代。(如圖十二)
圖十二:這一次 TS 終於認知到這兩個變數不能被其他型別給取代
如果今天想要讓某變數除了可以是 Nullable Types(可能真的就是代表空值),同時又是 —— 比如說,字串 string
這個型別。
以下測試將變數變成 string
型別但是沒有指派值。(如圖十三)
圖十三:先不指派值但是給予型別
你會發現,給它字串,它就可以正常運作。
然而再清除為 null
或 undefined
時又出現錯誤。其中的原因是:TS 已經認證該變數必須得是 string
的型態。
不過一些細心的讀者可能會發現:如果我們在還沒指派值之前先用變數的話,不就等同於它是 undefined
的值嗎?為何它不會在一開始就會拋出問題。筆者就來嘗試以下的程式碼:
你會發現 TS 這一次拋出了質疑(圖十四)。
圖十四:在還沒帶入值之前,中間如果被呼叫的話就會出現問題
哦!原來,TS 至少還會認得這個問題,這跟 TDZ (Temporal Dead Zone,暫時性死區)的概念還蠻像的。參照這個 Stackoverflow 的提問,裡面有句話把 TDZ 概念解釋得很清楚:
...,
let
andconst
are hoisted (likevar
,class
andfunction
), but there is a period between entering scope and being declared where they cannot be accessed. This period is the temporal dead zone (TDZ).
裡面提到 hoisted
這個單字,在理解 TDZ 時,讀者必須了解 JS 的變數作用域以及變數提升的概念。
由於這些是原生 JS 跟 ES6 let
的範疇,因此筆者不多作解釋。如果讀者不知道這個特性可以參考我放在小結最後的補充資料連結。反正這裡先丟個簡單的小結論:在未確定變數被正式丟入合法的值之前的這段空間,不能使用該變數。
讀者試試看
請問這段程式碼將
let
改回var
是不是也會出現類似的狀況呢?這個就由讀者來自行驗證看看囉。
注意
筆者常犯的英文拼寫錯誤,TDZ 全名是 Temporal Dead Zone,不是 Temperal
可是我們原先的目的是想讓變數除了可以成為我們指定的型別外,也可以成為 Nullable Type。這時候我們就需要特別使用 union
將該變數註記為 <YOUR-TYPE> | <nullable-type>
的格式。(如圖十五)
圖十五:絕對可以同時成為 string
或者是 null
由於我們已經讓該變數也可以成為 Nullable Type,最好的建議就是:除非可以直接指派值,否則初始化時,就指派 Nullable Type 的值。
重點 4. 對遲滯性指派進行型別註記
let A: T; A = B as T;
若使用者宣告變數
A
,其中沒有對A
指派值但明確給它型別註記T
,而後再把型別也是T
的B
變數代入A
的話:
- 就算
A
是非any
型別但一開始是undefined
的狀態,TS 仍然不會對你有什麼太大意見- 真正有意見時,是在你指派具
T
型別的值(也就是B
)到A
裡面前,你就對A
做其他行為,TS 會自動跟你槓上(TDZ 的概念)- 基本上,對
A
有註記跟沒註記T
型別差別僅僅只是防止變數A
被 TS 冷落(也就是被推論為any
),但也因為這樣我們才能找回 TS 對A
變數的關注,防止我們不小心弄錯A
的型別
重點 5. 型別註記的目的
- 其中註記最大的好處,除了是讓開發者明確知道變數固定在哪個型別外,TS 也可以不用猜就知道要怎麼幫我們關注該變數
- 把
any
這個禍根給剔除
重點 6. 型別註記與推論 Type Annotation & Inference
- TS 基本上會把型別推論做得很好,你也不太需要擔心說:“如果我忘記註記那裡我會不會得不到 TS 的關切?”,這問題對 TS 來講小事一樁,它可是很聰明的!
- 但是如果對於型別不明確或者是會被推論為
any
狀態的變數,你可就要積極使用型別註記囉,不然 TS 可不幫你處理這部分的事情,你也就不能怪 TS 說:“啊啊啊!這個爛煙蒂根本就沒用!”(那你就跟 TS 好好分手吧)- 型別推論是 TS 自你開始寫程式碼的時候,它就會幫你監控了;然而,型別註記則是開發者必須手動宣告給 TS 看的
光是講一個原始型別(Primitive Type)扯到的東西有夠多,不過這裡除了介紹完原始型別後,也順便用它們來展示 Type Inference 以及 Type Annotation 的機制。
當然,我們後續再談其他類型的推論與註記機制,會經過同樣的討論形式,但每種型別有各種細部問題等著讀者去發掘呢!
莫名其妙踩到雷
想想看,如果我們定義變數名稱為
name
,為何會在 TS 出錯呢?(有時候會出現錯誤,如果沒有遇到的話也可以想想看是什麼狀況下沒有錯誤)你可以試著上網查看看原因到底為何,這個就留待給讀者延伸探索囉~(如圖十六)
圖十六:這應該不成問題的啊!?